/* Copyright (c) Microsoft Corporation.
   Licensed under the MIT License. */

/***************************************************************************
	Author: ShonK
	Project: Kauai
	Reviewed:
	Copyright (c) Microsoft Corporation

	Code for implementing help balloons in kidspace.

***************************************************************************/
#include "kidframe.h"
ASSERTNAME

BEGIN_CMD_MAP(TXHG, TXRG)
	ON_CID_GEN(cidSelIdle, pvNil, pvNil)
	ON_CID_ME(cidActivateSel, pvNil, pvNil)
END_CMD_MAP_NIL()

RTCLASS(TXHD)
RTCLASS(TXHG)
RTCLASS(HBAL)
RTCLASS(HBTN)

const achar kchHelpString = '~';

/***************************************************************************
	Constructor for a help text document.
***************************************************************************/
TXHD::TXHD(PRCA prca, PDOCB pdocb, ulong grfdoc) : TXHD_PAR(pdocb, grfdoc)
{
	AssertPo(prca, 0);
	_prca = prca;
	_prca->AddRef();
	_htop.cnoBalloon = cnoNil;
	_htop.cnoScript = cnoNil;
	_htop.ckiSnd.ctg = ctgNil;
	_htop.ckiSnd.cno = cnoNil;
}


/***************************************************************************
	Destructor for a help text document.
***************************************************************************/
TXHD::~TXHD(void)
{
	ReleasePpo(&_prca);
}


#ifdef DEBUG
/***************************************************************************
	Assert the validity of a TXHD.
***************************************************************************/
void TXHD::AssertValid(ulong grf)
{
	TXHD_PAR::AssertValid(0);
	AssertPo(_prca, 0);
}


/***************************************************************************
	Mark memory for the TXHD.
***************************************************************************/
void TXHD::MarkMem(void)
{
	AssertValid(0);
	TXHD_PAR::MarkMem();
	MarkMemObj(_prca);
}
#endif //DEBUG


/***************************************************************************
	Static method to read a help text document from the given (pcfl, ctg, cno)
	and using the given prca as the source for pictures and buttons.
***************************************************************************/
PTXHD TXHD::PtxhdReadChunk(PRCA prca, PCFL pcfl, CTG ctg, CNO cno,
	PSTRG pstrg, ulong grftxhd)
{
	AssertPo(prca, 0);
	AssertPo(pcfl, 0);
	PTXHD ptxhd;

	if (pvNil == (ptxhd = NewObj TXHD(prca)) ||
			!ptxhd->_FReadChunk(pcfl, ctg, cno, pstrg, grftxhd))
		{
		PushErc(ercHelpReadFailed);
		ReleasePpo(&ptxhd);
		}

	AssertNilOrPo(ptxhd, fobjAssertFull);
	return ptxhd;
}


/***************************************************************************
	Read the given chunk into this TXRD.
***************************************************************************/
bool TXHD::_FReadChunk(PCFL pcfl, CTG ctg, CNO cno, PSTRG pstrg, ulong grftxhd)
{
	AssertPo(pcfl, 0);
	AssertNilOrPo(pstrg, 0);
	BLCK blck;
	KID kid;
	HTOPF htopf;
	long stid, lw;
	long cp, cpMac, cpMin;
	STN stn;
	bool fRet = fFalse;

	if (pcfl->FForest(ctg, cno))
		{
		CKI cki;

		if (pvNil == (pcfl = pcfl->PcflReadForest(ctg, cno, fFalse)))
			goto LFail;
		if (!pcfl->FGetCkiCtg(ctg, 0, &cki))
			goto LFail;
		cno = cki.cno;
		}
	else
		pcfl->AddRef();

	// The old version of HTOP didn't have the ckiSnd - accept both old and new
	// versions.
	htopf.htop.ckiSnd.ctg = ctgNil;
	htopf.htop.ckiSnd.cno = cnoNil;
	if (!pcfl->FFind(ctg, cno, &blck) || !blck.FUnpackData() ||
		size(HTOPF) != blck.Cb() && offset(HTOPF, htop.ckiSnd) != blck.Cb() ||
		!blck.FRead(&htopf))
		{
		goto LFail;
		}

	if (htopf.bo == kboOther)
		SwapBytesBom(&htopf.htop, kbomHtop);
	else if (htopf.bo != kboCur)
		goto LFail;

	if (!pcfl->FGetKidChidCtg(ctg, cno, 0, kctgRichText, &kid))
		goto LFail;

	if (!TXHD_PAR::_FReadChunk(pcfl, kid.cki.ctg, kid.cki.cno,
			FPure(grftxhd & ftxhdCopyText)))
		{
		goto LFail;
		}

	if ((grftxhd & ftxhdExpandStrings) && pvNil != pstrg)
		{
		SetInternal();
		SuspendUndo();
		cpMac = CpMac();
		for (cp = 0; cp < cpMac; )
			{
			if (_ChFetch(cp) != kchHelpString)
				{
				cp++;
				continue;
				}
	
			cpMin = cp++;
			for (stid = 0; cp < cpMac && FIn(lw = _ChFetch(cp) - '0', 0, 10); cp++)
				stid = stid * 10 + lw;
			if (!pstrg->FGet(stid, &stn))
				Warn("string missing");
			if (FReplaceRgch(stn.Psz(), stn.Cch(), cpMin, cp - cpMin, fdocNil))
				{
				cp = cpMin + stn.Cch();
				cpMac = CpMac();
				}
			}
		}

	fRet = fTrue;
	_htop = htopf.htop;
	AssertThis(0);

LFail:
	// Release our hold on the CFL
	ReleasePpo(&pcfl);

	return fRet;
}


/***************************************************************************
	Do any necessary munging of the AG entry on open.  Return false if
	we don't recognize this argument type.
***************************************************************************/
bool TXHD::_FOpenArg(long icact, byte sprm, short bo, short osk)
{
	CTG ctg;
	CNO cno;
	long cb;
	long rglw[2];
	long clw;

	if (TXHD_PAR::_FOpenArg(icact, sprm, bo, osk))
		return fTrue;

	cb = _pagcact->Cb(icact);
	switch (sprm)
		{
	case sprmGroup:
		if (cb < size(byte) + size(CNO))
			return fFalse;
		if (bo == kboOther)
			{
			_pagcact->GetRgb(icact, size(byte), size(CNO), &cno);
			SwapBytesRglw(&cno, 1);
			_pagcact->PutRgb(icact, size(byte), size(CNO), &cno);
			}
		break;

	case sprmObject:
		if (cb < size(CTG))
			return fFalse;
		_pagcact->GetRgb(icact, 0, size(CTG), &ctg);
		if (bo == kboOther)
			{
			SwapBytesRglw(&ctg, 1);
			_pagcact->PutRgb(icact, 0, size(CTG), &ctg);
			}
		cb -= size(CTG);

		switch (ctg)
			{
		case kctgMbmp:
		case kctgEditControl:
			clw = 1;
			goto LSwapBytes;

		case kctgGokd:
			clw = 2;
LSwapBytes:
			AssertIn(clw, 1, CvFromRgv(rglw) + 1);
			if (cb < clw * size(long))
				return fFalse;

			if (bo == kboOther)
				{
				_pagcact->GetRgb(icact, size(CTG), clw * size(long), rglw);
				SwapBytesRglw(rglw, clw);
				_pagcact->PutRgb(icact, size(CTG), clw * size(long), rglw);
				}
			break;

		default:
			return fFalse;
			}
		break;

	default:
		return fFalse;
		}

	return fTrue;
}


/***************************************************************************
	Save a help topic to the given chunky file.  Fill in *pcki with where
	we put the root chunk.
***************************************************************************/
bool TXHD::FSaveToChunk(PCFL pcfl, CKI *pcki, bool fRedirectText)
{
	AssertThis(0);
	AssertPo(pcfl, 0);
	AssertVarMem(pcki);
	BLCK blck;
	CKI cki;
	HTOPF htopf;

	pcki->ctg = kctgHelpTopic;
	htopf.bo = kboCur;
	htopf.osk = koskCur;
	htopf.htop = _htop;
	if (!pcfl->FAdd(size(HTOPF), pcki->ctg, &pcki->cno, &blck))
		{
		PushErc(ercHelpSaveFailed);
		return fFalse;
		}
	if (!blck.FWrite(&htopf))
		goto LFail;

	if (!TXHD_PAR::FSaveToChunk(pcfl, &cki, fRedirectText))
		goto LFail;

	//add the text chunk and write it
	if (!pcfl->FAdoptChild(pcki->ctg, pcki->cno, cki.ctg, cki.cno))
		{
		pcfl->Delete(cki.ctg, cki.cno);
LFail:
		pcfl->Delete(pcki->ctg, pcki->cno);
		PushErc(ercHelpSaveFailed);
		return fFalse;
		}

	return fTrue;
}


/***************************************************************************
	Get the bounding rectangle for the given object.
***************************************************************************/
bool TXHD::_FGetObjectRc(long icact, byte sprm, PGNV pgnv, PCHP pchp, RC *prc)
{
	AssertThis(0);
	AssertIn(icact, 0, _pagcact->IvMac());
	AssertIn(sprm, sprmMinObj, 0x100);
	AssertPo(pgnv, 0);
	AssertVarMem(pchp);
	AssertVarMem(prc);
	long cb;
	PMBMP pmbmp;
	PCRF pcrf;
	KID kid;
	long rglw[2];

	if (sprmObject != sprm)
		return fFalse;

	Assert(size(CTG) == size(long), 0);
	cb = _pagcact->Cb(icact);
	if (cb < size(rglw))
		return fFalse;
	_pagcact->GetRgb(icact, 0, size(rglw), rglw);
	switch ((CTG)rglw[0])
		{
	case kctgMbmp:
		pmbmp = (PMBMP)_prca->PbacoFetch(rglw[0], rglw[1], MBMP::FReadMbmp);
		goto LHaveMbmp;

	case kctgGokd:
		pcrf = _prca->PcrfFindChunk(rglw[0], rglw[1]);
		if (pvNil == pcrf)
			return fFalse;
		if (!pcrf->Pcfl()->FGetKidChidCtg(rglw[0], rglw[1],
				0x10000, kctgMbmp, &kid))
			{
			return fFalse;
			}
		pmbmp = (PMBMP)pcrf->PbacoFetch(kid.cki.ctg, kid.cki.cno, MBMP::FReadMbmp);
LHaveMbmp:
		if (pvNil == pmbmp)
			return fFalse;
		pmbmp->GetRc(prc);
		ReleasePpo(&pmbmp);
		prc->Offset(-prc->xpLeft, -prc->ypBottom);
		return fTrue;

	case kctgEditControl:
		pgnv->SetFont(pchp->onn, pchp->grfont, pchp->dypFont, tahLeft, tavBaseline);
		pgnv->GetRcFromRgch(prc, pvNil, 0);
		prc->Inset(0, -1);
		prc->xpLeft = 0;
		prc->xpRight = rglw[1];
		return fTrue;

	default:
		return fFalse;
		}
	return fTrue;
}


/***************************************************************************
	Draw the given object.
***************************************************************************/
bool TXHD::_FDrawObject(long icact, byte sprm, PGNV pgnv, long *pxp, long yp,
	PCHP pchp, RC *prcClip)
{
	AssertIn(icact, 0, _pagcact->IvMac());
	Assert(sprm >= sprmObject, 0);
	AssertPo(pgnv, 0);
	AssertVarMem(pxp);
	AssertVarMem(pchp);
	AssertVarMem(prcClip);
	long cb;
	RC rc;
	PMBMP pmbmp;
	PCRF pcrf;
	KID kid;
	long rglw[2];
	bool fDrawMbmp = fTrue;

	if (sprmObject != sprm)
		return fFalse;

	cb = _pagcact->Cb(icact);
	if (cb < size(rglw))
		return fFalse;
	_pagcact->GetRgb(icact, 0, size(rglw), rglw);
	switch ((CTG)rglw[0])
		{
	case kctgMbmp:
		pmbmp = (PMBMP)_prca->PbacoFetch(rglw[0], rglw[1], MBMP::FReadMbmp);
		goto LHaveMbmp;

	case kctgGokd:
		fDrawMbmp = !_fHideButtons;
		pcrf = _prca->PcrfFindChunk(rglw[0], rglw[1]);
		if (pvNil == pcrf)
			return fFalse;
		if (!pcrf->Pcfl()->FGetKidChidCtg(rglw[0], rglw[1],
				ChidFromSnoDchid(ksnoInit, 0), kctgMbmp, &kid))
			{
			return fFalse;
			}
		pmbmp = (PMBMP)pcrf->PbacoFetch(kid.cki.ctg, kid.cki.cno, MBMP::FReadMbmp);
LHaveMbmp:
		if (pvNil == pmbmp)
			return fFalse;
		pmbmp->GetRc(&rc);
		rc.Offset(*pxp - rc.xpLeft, yp - rc.ypBottom);
		if (kacrClear != pchp->acrBack)
			pgnv->FillRc(&rc, pchp->acrBack);
		if (fDrawMbmp)
			pgnv->DrawMbmp(pmbmp, &rc);
		ReleasePpo(&pmbmp);
		if (pchp->grfont & fontBoxed)
			{
			pgnv->SetPenSize(1, 1);
			pgnv->FrameRcApt(&rc, &vaptGray, pchp->acrFore, kacrClear);
			}
		*pxp += rc.Dxp();
		return fTrue;

	case kctgEditControl:
		pgnv->SetFont(pchp->onn, pchp->grfont, pchp->dypFont, tahLeft, tavBaseline);
		pgnv->GetRcFromRgch(&rc, pvNil, 0, 0, yp);
		rc.Inset(0, -1);
		rc.xpLeft = *pxp;
		rc.xpRight = rc.xpLeft + rglw[1];
		*pxp = rc.xpRight;
		pgnv->SetPenSize(1, 1);
		pgnv->FrameRc(&rc, kacrBlack);
		rc.Inset(1, 1);
		pgnv->FillRc(&rc, pchp->acrBack);
		return fTrue;

	default:
		return fFalse;
		}
	return fTrue;
}


/***************************************************************************
	Insert a picture into the help text document.
***************************************************************************/
bool TXHD::FInsertPicture(CNO cno, void *pvExtra, long cbExtra,
	long cp, long ccpDel, PCHP pchp, ulong grfdoc)
{
	AssertThis(0);
	AssertPvCb(pvExtra, cbExtra);
	AssertIn(cp, 0, CpMac());
	AssertIn(ccpDel, 0, CpMac() - cp);
	AssertNilOrVarMem(pchp);
	CKI cki;
	void *pv = &cki;
	bool fRet = fFalse;

	cki.ctg = kctgMbmp;
	cki.cno = cno;
	if (cbExtra > 0)
		{
		if (!FAllocPv(&pv, size(CKI) + cbExtra, fmemNil, mprNormal))
			return fFalse;
		CopyPb(&cki, pv, size(CKI));
		CopyPb(pvExtra, PvAddBv(pv, size(CKI)), cbExtra);
		}

	fRet = FInsertObject(pv, size(CKI) + cbExtra, cp, ccpDel, pchp, grfdoc);

	if (pv != &cki)
		FreePpv(&pv);
	return fRet;
}


/***************************************************************************
	Insert a new button
***************************************************************************/
bool TXHD::FInsertButton(CNO cno, CNO cnoTopic, void *pvExtra, long cbExtra,
	long cp, long ccpDel, PCHP pchp, ulong grfdoc)
{
	AssertThis(0);
	AssertPvCb(pvExtra, cbExtra);
	AssertIn(cp, 0, CpMac());
	AssertIn(ccpDel, 0, CpMac() - cp);
	AssertNilOrVarMem(pchp);
	byte rgb[size(CKI) + size(long)];
	CKI *pcki = (CKI *)rgb;
	CNO *pcnoTopic = (CNO *)(pcki + 1);;
	void *pv = rgb;
	bool fRet = fFalse;

	pcki->ctg = kctgGokd;
	pcki->cno = cno;
	*pcnoTopic = cnoTopic;
	if (cbExtra > 0)
		{
		if (!FAllocPv(&pv, size(rgb) + cbExtra, fmemNil, mprNormal))
			return fFalse;
		CopyPb(rgb, pv, size(rgb));
		CopyPb(pvExtra, PvAddBv(pv, size(rgb)), cbExtra);
		}

	fRet = FInsertObject(pv, size(rgb) + cbExtra, cp, ccpDel, pchp, grfdoc);

	if (pv != rgb)
		FreePpv(&pv);

	return fRet;
}


/***************************************************************************
	Group the given text into the given group.  lw == 0 indicates no group.
	Any non-zero number is a group.
***************************************************************************/
bool TXHD::FGroupText(long cp1, long cp2, byte bGroup, CNO cnoTopic,
	PSTN pstnTopic)
{
	AssertThis(0);
	AssertNilOrPo(pstnTopic, 0);
	AssertIn(cp1, 0, CpMac());
	AssertIn(cp2, 0, CpMac());
	SPVM spvm;

	SortLw(&cp1, &cp2);
	if (cp1 == cp2)
		return fTrue;

	if (!FSetUndo(cp1, cp2, cp2 - cp1))
		return fFalse;

	spvm.sprm = sprmGroup;
	spvm.lwMask = -1;
	if (bGroup == 0)
		{
		// means no grouping
		spvm.lw = 0;
		}
	else
		{
		byte rgb[size(byte) + size(CNO) + kcbMaxDataStn];
		long cb = size(byte) + size(CNO);

		rgb[0] = bGroup;
		CopyPb(&cnoTopic, rgb + size(byte), size(CNO));
		if (pvNil != pstnTopic && pstnTopic->Cch() > 0)
			{
			pstnTopic->GetData(rgb + cb);
			cb += pstnTopic->CbData();
			}

		if (!_FEnsureInAg(sprmGroup, rgb, cb, &spvm.lw))
			{
			CancelUndo();
			return fFalse;
			}
		}

	if (!_pglmpe->FEnsureSpace(2))
		{
		_ReleaseRgspvm(&spvm, 1);
		CancelUndo();
		return fFalse;
		}

	_ApplyRgspvm(cp1, cp2 - cp1, &spvm, 1);
	CommitUndo();

	AssertThis(0);
	InvalAllDdg(cp1, cp2 - cp1, cp2 - cp1);
	return fTrue;
}


/***************************************************************************
	Determine if the given cp is in a grouped text range.
***************************************************************************/
bool TXHD::FGrouped(long cp, long *pcpMin, long *pcpLim, byte *pbGroup,
	CNO *pcnoTopic, PSTN pstnTopic)
{
	AssertThis(0);
	AssertIn(cp, 0, CpMac());
	AssertNilOrVarMem(pcpMin);
	AssertNilOrVarMem(pcpLim);
	AssertNilOrVarMem(pbGroup);
	AssertNilOrVarMem(pcnoTopic);
	AssertNilOrPo(pstnTopic, 0);
	MPE mpe;
	byte bGroup = 0;

	if (!_FFindMpe(_SpcpFromSprmCp(sprmGroup, cp), &mpe, pcpLim))
		{
		mpe.lw = 0;
		mpe.spcp = 0;
		}

	if (mpe.lw > 0)
		{
		byte *prgb;
		long cb;

		prgb = (byte *)_pagcact->PvLock(mpe.lw - 1, &cb);
		cb -= size(byte) + size(CNO); // group number, cnoTopic
		if (cb < 0)
			goto LFail;

		bGroup = prgb[0];
		if (bGroup == 0)
			{
LFail:
			Bug("bad group data");
			_pagcact->Unlock();
			goto LNotGrouped;
			}

		if (pvNil != pcnoTopic)
			CopyPb(prgb + size(byte), pcnoTopic, size(CNO));
		if (pvNil != pstnTopic)
			{
			if (cb > 0)
				pstnTopic->FSetData(prgb + size(byte) + size(CNO), cb);
			else
				pstnTopic->SetNil();
			}
		_pagcact->Unlock();
		}
	else
		{
LNotGrouped:
		if (pvNil != pcnoTopic)
			*pcnoTopic = cnoNil;
		if (pvNil != pstnTopic)
			pstnTopic->SetNil();
		}

	if (pvNil != pbGroup)
		*pbGroup = bGroup;
	if (pvNil != pcpMin)
		*pcpMin = _CpFromSpcp(mpe.spcp);

	return bGroup != 0;
}


/***************************************************************************
	Get the help topic information.
***************************************************************************/
void TXHD::GetHtop(PHTOP phtop)
{
	AssertThis(0);
	AssertVarMem(phtop);

	*phtop = _htop;
}


/***************************************************************************
	Set the topic info.
***************************************************************************/
void TXHD::SetHtop(PHTOP phtop)
{
	AssertThis(0);
	AssertVarMem(phtop);

	_htop = *phtop;
	SetDirty();
}


/***************************************************************************
	Constructor for a TXHG.
***************************************************************************/
TXHG::TXHG(PWOKS pwoks, PTXHD ptxhd, PGCB pgcb) : TXRG(ptxhd, pgcb)
{
	AssertBaseThis(0);
	AssertPo(pwoks, 0);

	_pwoks = pwoks;
}


/***************************************************************************
	Create a new help topic display gob.
***************************************************************************/
PTXHG TXHG::PtxhgNew(PWOKS pwoks, PTXHD ptxhd, PGCB pgcb)
{
	PTXHG ptxhg;

	if (pvNil == (ptxhg = NewObj TXHG(pwoks, ptxhd, pgcb)))
		return pvNil;
	if (!ptxhg->_FInit())
		{
		ReleasePpo(&ptxhg);
		return pvNil;
		}
	return ptxhg;
}


/***************************************************************************
	Inititalize the display gob for a help balloon topic.
***************************************************************************/
bool TXHG::_FInit(void)
{
	AssertBaseThis(0);
	PRCA prca;
	long cp, cb;
	void *pv;
	CKI *pcki;
	long dxp;
	CNO cno;
	long xp, ypBase;
	CNO cnoTopic;
	byte bGroup;
	long lwMax;
	RTVN rtvn;
	long hid;
	CHP chp;
	RC rc;
	EDPAR edpar;
	STN stn;
	PTXHD ptxhd = Ptxhd();

	if (!TXHG_PAR::_FInit())
		return fFalse;

	// find the max of the group numbers
	lwMax = 0;
	for (cp = 0; cp < ptxhd->CpMac(); )
		{
		ptxhd->FGrouped(cp, pvNil, &cp, &bGroup);
		lwMax = LwMax((long)bGroup, lwMax);
		}

	// find a base hid that covers lwMax buttons
	_hidBase = 0;
	if (lwMax > 0)
		{
		_hidBase = CMH::HidUnique(lwMax) - 1;
		stn = PszLit("_gidBase");
		rtvn.SetFromStn(&stn);
		if (!FAssignRtvm(PgobPar()->Ppglrtvm(), &rtvn, _hidBase))
			return fFalse;
		}

	prca = Ptxhd()->Prca();
	for (cp = 0; Ptxhd()->FFetchObject(cp, &cp, &pv, &cb); cp++)
		{
		if (pvNil == pv)
			continue;

		if (cb < size(CTG))
			goto LContinue;

		switch (*(CTG *)pv)
			{
		case kctgEditControl:
			if (cb < size(ECOS))
				goto LContinue;
			dxp = ((ECOS *)pv)->dxp;
			FreePpv(&pv);

			// get the bounding rectangle
			_GetXpYpFromCp(cp, pvNil, pvNil, &xp, &ypBase, fFalse);
			_FetchChp(cp, &chp);
			_pgnv->SetFont(chp.onn, chp.grfont, chp.dypFont,
				tahLeft, tavBaseline);
			_pgnv->GetRcFromRgch(&rc, pvNil, 0);
			rc.Offset(0, ypBase + chp.dypOffset);
			rc.xpLeft = xp + 1;
			rc.xpRight = xp + dxp - 1;

			Ptxhd()->FGrouped(cp, pvNil, pvNil, &bGroup);
			if (bGroup == 0 ||
					_pwoks->PcmhFromHid(hid = _hidBase + bGroup) != pvNil)
				{
				hid = CMH::HidUnique();
				}
			if (chp.acrBack == kacrClear)
				chp.acrBack = kacrWhite;
			edpar.Set(hid, this, fgobNil, kginMark, &rc, pvNil, chp.onn,
				chp.grfont, chp.dypFont, tahLeft, tavTop,
				chp.acrFore, chp.acrBack);
			if (pvNil == EDSL::PedslNew(&edpar))
				return fFalse;
			break;

		case kctgGokd:
			if (cb < size(CKI) + size(CNO))
				goto LContinue;
			pcki = (CKI *)pv;
			cno = pcki->cno;
			cnoTopic = *(CNO *)(pcki + 1);
			FreePpv(&pv);

			_GetXpYpFromCp(cp, pvNil, pvNil, &xp, &ypBase, fFalse);
			_FetchChp(cp, &chp);
			Ptxhd()->FGrouped(cp, pvNil, pvNil, &bGroup,
				cnoTopic == cnoNil ? &cnoTopic : pvNil);
			if (bGroup == 0 ||
					_pwoks->PcmhFromHid(hid = _hidBase + bGroup) != pvNil)
				{
				hid = CMH::HidUnique();
				}
			if (pvNil == HBTN::PhbtnNew(_pwoks, this, hid, cno, prca, bGroup,
					cnoTopic, xp, ypBase + chp.dypOffset))
				{
				return fFalse;
				}
			break;

		default:
LContinue:
			FreePpv(&pv);
			break;
			}
		}

	AssertThis(0);
	return fTrue;
}


/***************************************************************************
	Return whether the point is over hot (marked text).
***************************************************************************/
bool TXHG::FPtIn(long xp, long yp)
{
	AssertThis(0);

	if (!TXHG_PAR::FPtIn(xp, yp))
		return fFalse;
	return FGroupFromPt(xp, yp);
}


/***************************************************************************
	Track the mouse.
***************************************************************************/
bool TXHG::FCmdTrackMouse(PCMD_MOUSE pcmd)
{
	AssertThis(0);
	AssertVarMem(pcmd);

	pcmd->grfcust = _pwoks->GrfcustAdjust(pcmd->grfcust);
	if (pcmd->cid == cidMouseDown)
		{
		// first response to mouse down
		Assert(vpcex->PgobTracking() == pvNil, "mouse already being tracked!");

		if (!FGroupFromPt(pcmd->xp, pcmd->yp, &_bTrack, &_cnoTrack))
			return fTrue;

		vpcex->TrackMouse(this);
		SetCursor(pcmd->grfcust);
		_grfcust = pcmd->grfcust;
		}
	else
		{
		Assert(vpcex->PgobTracking() == this, "not tracking mouse!");
		Assert(pcmd->cid == cidTrackMouse, 0);
		if (!(pcmd->grfcust & fcustMouse))
			{
			byte bGroup;
			CNO cnoTopic;
			vpcex->EndMouseTracking();

			if (FGroupFromPt(pcmd->xp, pcmd->yp, &bGroup, &cnoTopic) &&
					bGroup == _bTrack && cnoTopic == _cnoTrack)
				{
				DoHit(bGroup, cnoTopic, _grfcust, hidNil);
				}
			}
		}

	return fTrue;
}


/***************************************************************************
	An edit control got a bad key.
***************************************************************************/
bool TXHG::FCmdBadKey(PCMD_BADKEY pcmd)
{
	AssertThis(0);
	AssertVarMem(pcmd);

	if (!FIn(pcmd->hid, _hidBase + 1, _hidBase + 257))
		return fFalse;

	pcmd->grfcust = _pwoks->GrfcustAdjust(pcmd->grfcust);
	_FRunScript((byte)(pcmd->hid - _hidBase), pcmd->grfcust, pcmd->hid,
		(achar)pcmd->ch);
	return fTrue;
}


/***************************************************************************
	Return the number of the group text that the given point is in.
***************************************************************************/
bool TXHG::FGroupFromPt(long xp, long yp, byte *pbGroup, CNO *pcnoTopic)
{
	AssertThis(0);
	AssertNilOrVarMem(pbGroup);
	AssertNilOrVarMem(pcnoTopic);
	long cp;

	if (!_FGetCpFromPt(xp, yp, &cp, fFalse))
		return 0;
	return Ptxhd()->FGrouped(cp, pvNil, pvNil, pbGroup, pcnoTopic);
}


/***************************************************************************
	A child button was hit, take action.
***************************************************************************/
void TXHG::DoHit(byte bGroup, CNO cnoTopic, ulong grfcust, long hidHit)
{
	AssertThis(0);
	long lwRet = 0;

	// run the script
	if (!_FRunScript(bGroup, grfcust, hidHit, chNil, cnoTopic, &lwRet))
		return;

	if (cnoNil != cnoTopic && !lwRet)
		_pwoks->PhbalNew(PgobPar()->PgobPar(), Ptxhd()->Prca(), cnoTopic);
}


/***************************************************************************
	Run the script. Returns false iff the TXHG doesn't exist after
	running the script.
***************************************************************************/
bool TXHG::_FRunScript(byte bGroup, ulong grfcust, long hidHit, achar ch,
	CNO cnoTopic, long *plwRet)
{
	AssertThis(0);
	AssertNilOrVarMem(plwRet);

	PSCPT pscpt;
	PSCEG psceg;
	HTOP htop;
	bool fRet = fTrue;
	PTXHD ptxhd = Ptxhd();
	PRCA prca = ptxhd->Prca();

	if (pvNil != plwRet)
		*plwRet = 0;

	ptxhd->GetHtop(&htop);
	if (cnoNil == htop.cnoScript)
		return fTrue;

	pscpt = (PSCPT)prca->PbacoFetch(kctgScript, htop.cnoScript,
		SCPT::FReadScript);
	if (pvNil != pscpt && pvNil != (psceg = _pwoks->PscegNew(prca, this)))
		{
		AssertPo(pscpt, 0);
		AssertPo(psceg, 0);

		PWOKS pwoks = _pwoks;
		long grid = Grid();
		long rglw[5];

		rglw[0] = (long)bGroup;
		rglw[1] = grfcust;
		rglw[2] = hidHit;
		rglw[3] = (long)(byte)ch;
		rglw[4] = cnoTopic;

		//be careful not to use TXHG variables here in case the TXHG is
		//freed while the script is running.
		if (!psceg->FRunScript(pscpt, rglw, 5, plwRet) && pvNil != plwRet)
			*plwRet = 0;
		ReleasePpo(&psceg);

		fRet = (this == pwoks->PgobFromGrid(grid));
		}
	ReleasePpo(&pscpt);

	return fRet;
}


/***************************************************************************
	This handles cidMouseMove.
***************************************************************************/
bool TXHG::FCmdMouseMove(PCMD_MOUSE pcmd)
{
	AssertThis(0);
	AssertVarMem(pcmd);
	ulong grfcust = _pwoks->GrfcustAdjust(pcmd->grfcust);

	if (FGroupFromPt(pcmd->xp, pcmd->yp))
		grfcust |= fcustHotText;

	SetCursor(grfcust);
	return fTrue;
}


/***************************************************************************
	Set the cursor for this TXHG and the given cursor state.
***************************************************************************/
void TXHG::SetCursor(ulong grfcust)
{
	AssertThis(0);
	PGOB pgob;

	for (pgob = this; ; )
		{
		pgob = pgob->PgobPar();
		if (pvNil == pgob)
			{
			vpappb->SetCurs(pvNil);
			break;
			}
		if (pgob->FIs(kclsGOK))
			{
			((PGOK)pgob)->SetCursor(grfcust | fcustChildGok);
			break;
			}
		}
}


/***************************************************************************
	Create a new help topic balloon based on the given topic number.
***************************************************************************/
PHBAL HBAL::PhbalCreate(PWOKS pwoks, PGOB pgobPar, PRCA prca,
	CNO cnoTopic, PHTOP phtop)
{
	AssertPo(pwoks, 0);
	AssertPo(pgobPar, 0);
	AssertPo(prca, 0);
	AssertNilOrVarMem(phtop);
	PCRF pcrf;
	PTXHD ptxhd;
	PHBAL phbal;

	pcrf = prca->PcrfFindChunk(kctgHelpTopic, cnoTopic);
	if (pvNil == pcrf)
		return pvNil;

	ptxhd = TXHD::PtxhdReadChunk(prca, pcrf->Pcfl(), kctgHelpTopic, cnoTopic,
		pwoks->Pstrg());
	if (pvNil == ptxhd)
		return pvNil;

	ptxhd->HideButtons();
	phbal = PhbalNew(pwoks, pgobPar, prca, ptxhd, phtop);
	ReleasePpo(&ptxhd);

	return phbal;
}


/***************************************************************************
	Static method to create a new help balloon based on the given help
	topic document and htop.
***************************************************************************/
PHBAL HBAL::PhbalNew(PWOKS pwoks, PGOB pgobPar, PRCA prca,
	PTXHD ptxhd, PHTOP phtop)
{
	AssertPo(pwoks, 0);
	AssertPo(pgobPar, 0);
	AssertPo(ptxhd, 0);
	AssertPo(prca, 0);
	AssertNilOrVarMem(phtop);
	HTOP htop;
	GCB gcb;
	PHBAL phbal;
	long grid;

	ptxhd->GetHtop(&htop);
	if (pvNil != phtop)
		{
		// merge the given htop with the topic's htop.
		if (cnoNil != phtop->cnoBalloon)
			htop.cnoBalloon = phtop->cnoBalloon;
		if (hidNil != phtop->hidThis)
			htop.hidThis = phtop->hidThis;
		if (hidNil != phtop->hidTarget)
			htop.hidTarget = phtop->hidTarget;
		if (cnoNil != phtop->cnoScript)
			htop.cnoScript = phtop->cnoScript;
		htop.dxp += phtop->dxp;
		htop.dyp += phtop->dyp;
		if (cnoNil != phtop->ckiSnd.cno && ctgNil != phtop->ckiSnd.ctg)
			htop.ckiSnd = phtop->ckiSnd;
		}

	if (htop.hidThis == hidNil)
		htop.hidThis = CMH::HidUnique();
	else if (pvNil != (phbal = (PHBAL)pwoks->PcmhFromHid(htop.hidThis)))
		{
		if (!phbal->FIs(kclsHBAL))
			{
			Bug("command handler with this ID already exists");
			return pvNil;
			}

		AssertPo(phbal, 0);

#ifdef REVIEW //shonk: this makes little sense and is bug-prone
		if (htop.cnoBalloon == phbal->_pgokd->Cno() && prca == phbal->_prca)
			{
			// same hid, same GOKD, same prca, so just change the topic
			if (!phbal->FSetTopic(ptxhd, &htop, prca))
				return pvNil;
			return phbal;
			}
#endif //REVIEW

		// free the balloon and create the new one.
		ReleasePpo(&phbal);
		}

	gcb.Set(htop.hidThis, pgobPar, fgobNil, kginMark);
	if (pvNil == (phbal = NewObj HBAL(&gcb)))
		return pvNil;
	grid = phbal->Grid();

	if (!phbal->_FInit(pwoks, ptxhd, &htop, prca))
		{
		ReleasePpo(&phbal);
		return pvNil;
		}

	if (!phbal->_FEnterState(ksnoInit))
		{
		Warn("HBAL immediately destroyed!");
		return pvNil;
		}

	// initialize the topic
	phbal->_ptxhg->DoHit(0, cnoNil, fcustNil, hidNil);
	if (phbal != pwoks->PgobFromGrid(grid))
		{
		Warn("HBAL immediately destroyed 2!");
		return pvNil;
		}

	AssertPo(phbal, 0);
	return phbal;
}


/***************************************************************************
	Constructor for a help balloon.
***************************************************************************/
HBAL::HBAL(GCB *pgcb) : HBAL_PAR(pgcb)
{
}


/***************************************************************************
	Initialize the help balloon.
***************************************************************************/
bool HBAL::_FInit(PWOKS pwoks, PTXHD ptxhd, HTOP *phtop, PRCA prca)
{
	AssertBaseThis(0);
	AssertPo(ptxhd, 0);
	AssertVarMem(phtop);
	AssertPo(prca, 0);

	if (!HBAL_PAR::_FInit(pwoks, phtop->cnoBalloon, prca))
		return fFalse;

	return _FSetTopic(ptxhd, phtop, prca);
}


/***************************************************************************
	Set the topic for this balloon.  Returns false if setting the topic
	fails or if the balloon is instantly killed by a script.
***************************************************************************/
bool HBAL::FSetTopic(PTXHD ptxhd, PHTOP phtop, PRCA prca)
{
	AssertThis(0);
	AssertPo(ptxhd, 0);
	AssertVarMem(phtop);
	AssertPo(prca, 0);

	if (!_FSetTopic(ptxhd, phtop, prca))
		return fFalse;

	return _FEnterState(ksnoInit);
}


/***************************************************************************
	Set the topic in the help balloon.  Don't enter the initial state.
***************************************************************************/
bool HBAL::_FSetTopic(PTXHD ptxhd, PHTOP phtop, PRCA prca)
{
	AssertBaseThis(0);
	AssertPo(ptxhd, 0);
	AssertVarMem(phtop);
	AssertPo(prca, 0);

	PGOB pgob;
	GCB gcb;
	PT pt, ptReg;
	STN stn;
	RTVN rtvn;
	PTXHG ptxhgSave = _ptxhg;

	// create the topic DDG.
	gcb.Set(CMH::HidUnique(), this, fgobNil, kginMark);
	if (pvNil == (_ptxhg = TXHG::PtxhgNew(_pwoks, ptxhd, &gcb)))
		goto LFail;

	// set the sound variables
	stn = PszLit("_ctgSound");
	rtvn.SetFromStn(&stn);
	if (!FAssignRtvm(_ptxhg->Ppglrtvm(), &rtvn, phtop->ckiSnd.ctg))
		goto LFail;
	stn = PszLit("_cnoSound");
	rtvn.SetFromStn(&stn);
	if (!FAssignRtvm(_ptxhg->Ppglrtvm(), &rtvn, phtop->ckiSnd.cno))
		{
LFail:
		ReleasePpo(&_ptxhg);

		// restore the previous topic DDG
		_ptxhg = ptxhgSave;
		return fFalse;
		}
	ReleasePpo(&ptxhgSave);

	_ptxhg->GetNaturalSize(&_dxpPref, &_dypPref);
	if (hidNil == phtop->hidTarget ||
			pvNil == (pgob = _pwoks->PgobFromHid(phtop->hidTarget)))
		{
		pgob = PgobPar();
		}

	if (pgob->FIs(kclsGOK))
		((PGOK)pgob)->GetPtReg(&pt);
	else
		{
		RC rc;

		pgob->GetRc(&rc, cooParent);
		pt.xp = rc.XpCenter();
		pt.yp = rc.YpCenter();
		}
	pgob->MapPt(&pt, cooParent, cooGlobal);

	// point the balloon at the gob
	PgobPar()->MapPt(&pt, cooGlobal, cooLocal);
	GetPtReg(&ptReg);
	_SetGorp(_pgorp, pt.xp - ptReg.xp + phtop->dxp,
		pt.yp - ptReg.yp + phtop->dyp);

	return fTrue;
}


/***************************************************************************
	Our representation is changing, so make sure we stay inside our parent
	and reposition the TXHG.
***************************************************************************/
void HBAL::_SetGorp(PGORP pgorp, long dxp, long dyp)
{
	RC rc1, rc2, rc3;

	HBAL_PAR::_SetGorp(pgorp, dxp, dyp);

	// make sure we stay inside our parent
	GetRc(&rc1, cooParent);
	PgobPar()->GetRc(&rc2, cooLocal);
	rc3.FIntersect(&rc1, &rc2);
	if (rc3 != rc1)
		{
		rc1.PinToRc(&rc2);
		SetPos(&rc1);
		}

	// position the TXHG.
	GetRcContent(&rc1);
	rc2.Set(0, 0, _dxpPref, _dypPref);
	rc2.CenterOnRc(&rc1);
	_ptxhg->SetPos(&rc2);
}


/***************************************************************************
	Constructor for a help balloon button.
***************************************************************************/
HBTN::HBTN(GCB *pgcb) : HBTN_PAR(pgcb)
{
}


/***************************************************************************
	Create a new help balloon button
***************************************************************************/
PHBTN HBTN::PhbtnNew(PWOKS pwoks, PGOB pgobPar, long hid, CNO cno, PRCA prca,
	byte bGroup, CNO cnoTopic, long xpLeft, long ypBottom)
{
	AssertPo(pwoks, 0);
	AssertNilOrPo(pgobPar, 0);
	Assert(hid != hidNil, "nil ID");
	AssertPo(prca, 0);
	GCB gcb;
	PHBTN phbtn;
	RC rcAbs;

	if (pvNil != pwoks->PcmhFromHid(hid))
		{
		Bug("command handler with this ID already exists");
		return pvNil;
		}

	gcb.Set(hid, pgobPar, fgobNil, kginMark);
	if (pvNil == (phbtn = NewObj HBTN(&gcb)))
		return pvNil;

	phbtn->_bGroup = bGroup;
	phbtn->_cnoTopic = cnoTopic;
	if (!phbtn->_FInit(pwoks, cno, prca))
		{
		ReleasePpo(&phbtn);
		return pvNil;
		}

	if (!phbtn->_FEnterState(ksnoInit))
		{
		Warn("GOK immediately destroyed!");
		return pvNil;
		}
	phbtn->GetRc(&rcAbs, cooParent);
	rcAbs.Offset(xpLeft - rcAbs.xpLeft, ypBottom - rcAbs.ypBottom);
	phbtn->SetPos(&rcAbs, pvNil);

	AssertPo(phbtn, 0);
	return phbtn;
}


/***************************************************************************
	Test whether the given point is in this button or its related text.
***************************************************************************/
bool HBTN::FPtIn(long xp, long yp)
{
	AssertThis(0);
	PTXHG ptxhg;
	PT pt(xp, yp);
	byte bGroup;
	CNO cnoTopic;

	if (HBTN_PAR::FPtIn(xp, yp))
		return fTrue;

	if (_bGroup == 0 || !PgobPar()->FIs(kclsTXHG))
		return fFalse;

	ptxhg = (PTXHG)PgobPar();
	MapPt(&pt, cooLocal, cooParent);

	if (!ptxhg->FGroupFromPt(pt.xp, pt.yp, &bGroup, &cnoTopic))
		return fFalse;
	return bGroup == _bGroup && cnoTopic == _cnoTopic;
}


/***************************************************************************
	The button has been clicked on.  Tell the TXHG to do its thing.
***************************************************************************/
bool HBTN::FCmdClicked(PCMD_MOUSE pcmd)
{
	AssertThis(0);
	AssertVarMem(pcmd);

	PTXHG ptxhg;
	long hid = Hid();

	if (!PgobPar()->FIs(kclsTXHG))
		{
		Bug("why isn't my parent a TXHG?");
		return fTrue;
		}

	ptxhg = (PTXHG)PgobPar();
	ptxhg->DoHit(_bGroup, _cnoTopic, pcmd->grfcust, hid);

	return fTrue;
}

